package com.gitee.wsl.compose.chart.xy.bar.bullet

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.SubcomposeMeasureScope
import androidx.compose.ui.unit.Constraints
import com.gitee.wsl.compose.chart.ExperimentalKoalaPlotApi
import com.gitee.wsl.compose.chart.xy.axis.style.Axis
import com.gitee.wsl.compose.chart.xy.axis.style.AxisDelegate
import com.gitee.wsl.compose.chart.xy.axis.style.AxisStyle
import com.gitee.wsl.compose.chart.xy.axis.LinearAxisModel
import com.gitee.wsl.compose.chart.xy.axis.style.rememberAxisStyle
import com.gitee.wsl.compose.chart.xy.bar.bullet.indicator.DiamondIndicator
import com.gitee.wsl.compose.chart.xy.bar.bullet.indicator.HorizontalBarIndicator
import com.gitee.wsl.compose.chart.xy.bar.bullet.indicator.LineIndicator
import com.gitee.wsl.compose.chart.xy.bar.bullet.model.Range
import com.gitee.wsl.compose.chart.xy.bar.bullet.model.RangesScope
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt



private const val FeaturedMeasureDefaultSize = 0.33f

private val DefaultRangeShades = listOf(
    listOf(0.65f),
    listOf(0.65f, 0.9f),
    listOf(0.6f, 0.75f, 0.9f),
    listOf(0.5f, 0.65f, 0.8f, 0.9f),
    listOf(0.5f, 0.65f, 0.8f, 0.9f, 0.97f),
)

private const val MinRangeShade = 0.99f



/**
 * A scope for configuring a bullet graph.
 *
 * @param axisModel Sets the LinearAxisModel to use for the graph.
 *
 */
@ExperimentalKoalaPlotApi
@BulletGraphDslMarker
class BulletBuilderScope<T>(
    val axisModel: LinearAxisModel<T>
) where T : Comparable<T>, T : Number {

    internal val comparativeMeasures: MutableList<ComparativeMeasure<T>> = mutableListOf()
    internal var featuredMeasure: FeaturedMeasure<T>? = null
    internal var rangesScope: RangesScope<T> = RangesScope<T>()

    /**
     * Content for the bullet graph's label.
     */
     val label: Slot<@Composable () -> Unit> = Slot {}

    /**
     * Specifies the axis settings and content. See [AxisSettings].
     */
     val axis: Slot<AxisSettings<T>.() -> Unit> = Slot {}

    /**
     * Sets the comparative measure value and the composable used to draw the indicator. The composable
     * will be centered on the value with a width and height equal to the height of the
     * BulletGraph background fill area.
     */
     fun comparativeMeasure(value: T, indicator: @Composable () -> Unit = { LineIndicator() }) {
        comparativeMeasures += ComparativeMeasure(value, indicator)
    }

    /**
     * Sets the feature measure value and indicator as a bar.
     *
     * @param value The value of the featured measure
     * @param indicator The composable used to render the featured measure. For a bar, the
     * width of the composable represents the extent of the featured measure from 0 to value.
     */
     fun featuredMeasureBar(
        value: T,
        indicator: @Composable () -> Unit = {
            HorizontalBarIndicator(SolidColor(MaterialTheme.colorScheme.primary), fraction = FeaturedMeasureDefaultSize)
        }
    ) {
        featuredMeasure = FeaturedMeasure(value, FeaturedMeasureType.BAR, indicator)
    }

    /**
     * Sets the feature measure value and indicator as a symbol.
     *
     * @param value The value of the featured measure
     * @param indicator The composable used to render the featured measure. For a
     * symbol, the composable is centered on value with a height and width equal to the
     * height of the BulletGraph background fill area.
     */
     fun featuredMeasureSymbol(
        value: T,
        indicator: @Composable () -> Unit = { DiamondIndicator() }
    ) {
        featuredMeasure = FeaturedMeasure(value, FeaturedMeasureType.SYMBOL, indicator)
    }


    /**
     * Defines the qualitative ranges starting at the specified [start] value. See [RangesScope] for how to
     * configure individual range values and their graphical representation. If the default visual representation is
     * desired then the overloaded version of this function that takes only a variable number of Float values may be
     * used instead.
     */
     fun ranges(start: T, block: RangesScope<T>.() -> Unit) {
        rangesScope = RangesScope<T>().apply {
            range(start)
            block()
        }
    }

    /**
     * Defines the qualitative ranges using the default visual representation. If the visual representation needs
     * to be customized then use the overloaded version of this function.
     */
     fun ranges(vararg values: T) {
        rangesScope = RangesScope<T>().apply {
            values.forEach {
                ranges.add(Range<T>(it, null))
            }
        }
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || this::class != other::class) return false

        other as BulletBuilderScope<*>

        if (comparativeMeasures != other.comparativeMeasures) return false
        if (featuredMeasure != other.featuredMeasure) return false
        if (rangesScope != other.rangesScope) return false
        if (label != other.label) return false
        return axis == other.axis
    }

    override fun hashCode(): Int {
        var result = comparativeMeasures.hashCode()
        result = 31 * result + featuredMeasure.hashCode()
        result = 31 * result + rangesScope.hashCode()
        result = 31 * result + label.hashCode()
        result = 31 * result + axis.hashCode()
        return result
    }

    @Composable
    internal fun createBulletGraphBuilder(
        slotId: Int
    ): BulletGraphBuilder<T> {
        return BulletGraphBuilder(
            slotId,
            AxisSettings.fromScope(this),
            this
        )
    }

}

enum class Slots {
    AXIS_LABELS,
    AXIS,
    RANGES,
    FEATURE,
    COMPARATIVE
}


@OptIn(ExperimentalKoalaPlotApi::class)
internal class BulletGraphBuilder<T>(
    private val slotId: Int,
    private val axisSettings: AxisSettings<T>,
    val builderScope: BulletBuilderScope<T>
) where T : Comparable<T>, T : Number{

    internal var bulletHeight: Int = 0

    var labelPlaceable: Placeable? = null
    var axis: AxisDelegate<T>? = null
    var axisLabelPlaceables: List<Placeable>? = null
    private var axisHeight: Int? = null
    private var axisPlaceable: Placeable? = null
    private val axisLabelsHeight: Int by lazy {
        requireNotNull(axisLabelPlaceables)
        axisLabelPlaceables?.let { placeables ->
            placeables.maxOfOrNull { it.height }
        } ?: 0
    }
    private val rangeHeight: Int by lazy {
        requireNotNull(axisHeight) { "axisHeight must not be null in order to calculate rangeHeight" }
        (bulletHeight - axisHeight!! - axisLabelsHeight).coerceAtLeast(0)
    }
    private var rangePlaceables: List<Placeable>? = null
    private var featurePlaceable: Placeable? = null
    private var comparativeMeasurePlaceables: List<Placeable>? = null

    @OptIn(ExperimentalKoalaPlotApi::class)
    fun measureLabel(scope: SubcomposeMeasureScope, labelWidthMaxConstraint: Int) {
        labelPlaceable = scope.measureLabel(builderScope, Constraints(maxWidth = labelWidthMaxConstraint, maxHeight = bulletHeight))
    }

    private fun slotId(slot: Slots): String {
        return "$slotId.${slot.name}"
    }

    /**
     * @param width of the non-label graph area: constraints.maxWidth - labelWidth
     */
    @OptIn(ExperimentalKoalaPlotApi::class)
    fun measureAxisLabels(scope: SubcomposeMeasureScope, width: Int) {
        with(scope) {
            val axis = AxisDelegate.createHorizontalAxis(
                builderScope.axisModel,
                axisSettings.style!!,
                width.toDp()
            )
            axisHeight = (axis.thicknessDp - axis.axisOffset).toPx().roundToInt()

            val measurable = subcompose(slotId(Slots.AXIS_LABELS)) {
                axis.majorTickValues.forEach { Box { axisSettings.label(it) } }
            }
            axisLabelPlaceables = measurable.map {
                it.measure(
                    Constraints(
                        maxWidth = (width / axis.majorTickValues.size).coerceAtLeast(0),
                        maxHeight = bulletHeight,
                    )
                )
            }

            this@BulletGraphBuilder.axis = axis
        }
    }

    /**
     * @param rangeWidth Width of the axis
     */
    fun measureAxis(scope: SubcomposeMeasureScope, rangeWidth: Int) {
        requireNotNull(axis) { "axis must not be null" }
        axisPlaceable = scope.subcompose(slotId(Slots.AXIS)) { Axis(axis!!) }[0].measure(Constraints.fixedWidth(rangeWidth))
    }

    @OptIn(ExperimentalKoalaPlotApi::class)
    fun measureRanges(scope: SubcomposeMeasureScope, rangeWidth: Int) {
        val rangeMeasurables: List<Measurable> =
            scope.subcompose(slotId(Slots.RANGES)) {
                RangeIndicators(builderScope)
            }

        val ranges = builderScope.rangesScope.ranges
        with(builderScope){
            rangePlaceables = rangeMeasurables.mapIndexed { rangeIndex, measurable ->
                val maxWidth = ((rangeWidth * axisModel.computeOffset(ranges[rangeIndex + 1].value)).roundToInt() -
                        (rangeWidth * axisModel.computeOffset(ranges[rangeIndex].value)).roundToInt()
                        ).absoluteValue
                measurable.measure(Constraints.fixed(maxWidth, rangeHeight))
            }
        }
    }

    @OptIn(ExperimentalKoalaPlotApi::class)
    @Composable
    private fun <T> RangeIndicators(builtScope: BulletBuilderScope<T>) where T : Comparable<T>, T : Number {
        val shadeIndex = max(builtScope.rangesScope.ranges.size - 1, DefaultRangeShades.lastIndex)
        val numShades = DefaultRangeShades[shadeIndex].size
        for (rangeIndex in 1 until builtScope.rangesScope.ranges.size) {
            val range = builtScope.rangesScope.ranges[rangeIndex]
            if (range.indicator == null) {
                val shade = if (rangeIndex - 1 >= numShades) {
                    MinRangeShade
                } else {
                    DefaultRangeShades[shadeIndex][rangeIndex - 1]
                }
                HorizontalBarIndicator(SolidColor(Color(shade, shade, shade)))
            } else {
                Box(modifier = Modifier.fillMaxSize()) { range.indicator.invoke() }
            }
        }
    }

    @OptIn(ExperimentalKoalaPlotApi::class)
    fun measureFeature(scope: SubcomposeMeasureScope, rangeWidth: Int, animationSpec: AnimationSpec<Float>) {
        builderScope.featuredMeasure?.let { featuredMeasure ->
            val origin = computeFeatureBarOrigin(featuredMeasure.value, builderScope.axisModel.range)
            val featureWidth = rangeWidth * abs(
                builderScope.axisModel.computeOffset(featuredMeasure.value) - builderScope.axisModel.computeOffset(
                    origin
                )
            )

            val measurable = scope.subcompose(slotId(Slots.FEATURE)) {
                // Animation scale factor
                val beta = remember(featuredMeasure) { Animatable(0f) }
                LaunchedEffect(featuredMeasure) {
                    beta.animateTo(1f, animationSpec = animationSpec)
                }

                with(scope) {
                    val sizeModifier =
                        if (featuredMeasure.type == FeaturedMeasureType.BAR) {
                            Modifier.size((beta.value * featureWidth).toDp(), rangeHeight.toDp())
                        } else {
                            Modifier.size(rangeHeight.toDp(), rangeHeight.toDp())
                        }

                    Box(
                        contentAlignment = Alignment.Center,
                        modifier = sizeModifier
                    ) { featuredMeasure.indicator() }
                }
            }[0]

            featurePlaceable = measurable.measure(Constraints(maxWidth = rangeWidth, maxHeight = rangeHeight))
        }
    }

    @OptIn(ExperimentalKoalaPlotApi::class)
    fun measureComparativeMeasures(scope: SubcomposeMeasureScope) {
        val measurables: List<Measurable> =
            scope.subcompose(slotId(Slots.COMPARATIVE)) {
                builderScope.comparativeMeasures.forEach {
                    Box(contentAlignment = Alignment.Center) { it.indicator() }
                }
            }

        comparativeMeasurePlaceables = measurables.map {
            it.measure(Constraints.fixed(rangeHeight, rangeHeight))
        }
    }

    @OptIn(ExperimentalKoalaPlotApi::class)
    fun Placeable.PlacementScope.layout(
        yPos: Int,
        labelWidth: Int,
        firstAxisLabelWidth: Int,
        rangeWidth: Int
    ) {
        requireNotNull(axis) { "axis must not be null during layout" }
        requireNotNull(axisLabelPlaceables) { "axis label placeables must not be null during layout" }
        requireNotNull(labelPlaceable) { "labelPlacesable must not be null during layout" }
        requireNotNull(axisPlaceable) { "axisPlaceable must not be null during layout" }
        requireNotNull(rangePlaceables) { "rangePlaceables must not be null during layout" }
        requireNotNull(comparativeMeasurePlaceables) {
            "comparativeMeasurePlaceables must not be null during layout"
        }

        val axis = axis!!
        val axisLabelPlaceables = axisLabelPlaceables!!
        val labelPlaceable = labelPlaceable!!
        val axisPlaceable = axisPlaceable!!
        val rangePlaceables = rangePlaceables!!
        val comparativeMeasurePlaceables = comparativeMeasurePlaceables!!

        val rangeStart = labelWidth + firstAxisLabelWidth / 2

        labelPlaceable.place(
            labelWidth - labelPlaceable.width,
            (yPos + bulletHeight / 2 - labelPlaceable.height / 2).coerceAtLeast(0)
        )

        axisPlaceable.place(rangeStart, yPos + rangeHeight)

        with(builderScope){
            // place axis labels
            axis.majorTickValues.forEachIndexed { index, fl ->
                axisLabelPlaceables[index].place(
                    (rangeStart + rangeWidth * axisModel.computeOffset(fl) - axisLabelPlaceables[index].width / 2).roundToInt(),
                    yPos + (bulletHeight - axisLabelsHeight).coerceAtLeast(0)
                )
            }

            // place range indicators
            rangePlaceables.forEachIndexed { index, placeable ->
                val xPos = rangeStart + rangeWidth * axisModel.computeOffset(builderScope.rangesScope.ranges[index].value)
                placeable.place(xPos.roundToInt(), yPos)
            }

            // place comparative measures
            comparativeMeasurePlaceables.forEachIndexed { index, placeable ->
                val xPos = rangeStart + rangeWidth * axisModel.computeOffset(builderScope.comparativeMeasures[index].value) - placeable.width / 2
                placeable.place(xPos.roundToInt(), yPos)
            }

            // place feature
            featurePlaceable?.let {
                val value = builderScope.featuredMeasure!!.value
                val xPos = rangeStart + if (builderScope.featuredMeasure!!.type == FeaturedMeasureType.BAR) {
                    val origin = computeFeatureBarOrigin(value, axisModel.range)
                    if (value.toDouble() < 0) {
                        rangeWidth * axisModel.computeOffset(origin) - it.width
                    } else {
                        rangeWidth * axisModel.computeOffset(origin)
                    }
                } else {
                    rangeWidth * axisModel.computeOffset(value) - it.width / 2
                }
                it.place(xPos.roundToInt(), yPos + rangeHeight / 2 - it.height / 2)
            }
        }
    }

    /**
     * Technically per the BulletGraph spec a bar is not to be used if the origin of the axis does not include
     * 0, but this will force the feature bar to have an origin of 0 or a bound of the axis in order to display
     * in a reasonable way.
     * @param featuredMeasureValue The value of the featured measure
     * @param axisRange the range of the axis
     */
    private fun computeFeatureBarOrigin(featuredMeasureValue: T, axisRange: ClosedRange<T>): T {
        val r = if (featuredMeasureValue.toDouble() >= 0) {
            max(0.0, axisRange.start.toDouble())
        } else {
            min(0.0, axisRange.endInclusive.toDouble())
        }

        return when (featuredMeasureValue) {
            is Int -> r.toInt()
            is Long -> r.toLong()
            is Double -> r
            is Float -> r.toFloat()
            is Short -> r.toInt().toShort()
            is Byte -> r.toInt().toByte()
            else -> {
                throw IllegalArgumentException("Unexpected Number type")
            }
        } as T
    }
}

@OptIn(ExperimentalKoalaPlotApi::class)
internal fun <T> SubcomposeMeasureScope.measureLabel(
    bulletScope: BulletBuilderScope<T>,
    constraints: Constraints
): Placeable where T : Comparable<T>, T : Number {
    return with(bulletScope) {
        val labelMeasurable = subcompose(label) { Box { label.value() } }[0]
        labelMeasurable.measure(constraints)
    }
}

/**
 * A slot to hold a configurable value in the bullet graph dsl.
 * @param default The default value held by the Slot
 * @param T The type of the value held by the Slot
 */
@ExperimentalKoalaPlotApi
@BulletGraphDslMarker
open class Slot<T>(default: T) {
    internal var value: T = default

    /**
     * Sets the Slot's value.
     * @param setValue The value to set.
     */
     operator fun invoke(setValue: T) {
        value = setValue
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || this::class != other::class) return false

        other as Slot<*>

        if (value != other.value) return false

        return true
    }

    override fun hashCode(): Int {
        return value?.hashCode() ?: 0
    }
}

/**
 * Specifies the settings for an Axis.
 */
@ExperimentalKoalaPlotApi
@BulletGraphDslMarker
class AxisSettings<T> where T : Comparable<T>, T : Number {
    /**
     * Content for the bullet graph's axis labels.
     */
     val labels: Slot<@Composable (T) -> Unit> = Slot {}

    /**
     * Shortcut to call the label Composable stored in the labels Slot.
     */
    @Composable
    internal infix fun label(f: T) {
        labels.value(f)
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || this::class != other::class) return false

        other as AxisSettings<*>

        if (labels != other.labels) return false
        return style == other.style
    }

    override fun hashCode(): Int {
        var result = labels.hashCode()
        result = 31 * result + (style?.hashCode() ?: 0)
        return result
    }

    /**
     * Optional styling for the bullet graph axis. If not provided, default styling will be used.
     */
     var style: AxisStyle? = null

    internal companion object {
        @Composable
        fun <T> fromScope(bulletScope: BulletBuilderScope<T>): AxisSettings<T> where T : Comparable<T>, T : Number {
            val settings = AxisSettings<T>().apply(bulletScope.axis.value)
            // settings.model = settings.model ?: createLinearAxisModel(bulletScope)
            settings.style = settings.style ?: rememberAxisStyle()
            return settings
        }

//        private fun <T> createLinearAxisModel(builtScope: BulletBuilderScope<T>):
        //        FloatLinearAxisModel where T : Comparable<T>, T : Number {
//            val range = (
//                builtScope.rangesScope.ranges.map { it.value } +
//                    builtScope.featuredMeasure.value +
//                    builtScope.comparativeMeasures.map { it.value }
//                ).autoScaleRange()
//
//            return FloatLinearAxisModel(range, allowZooming = false, allowPanning = false)
//        }
    }
}

@DslMarker
annotation class BulletGraphDslMarker
